/* ohaigent
 *
 * The guts of the ohai jabber/xmpp commandline suite.
 * ohaigent maintains a client connection to your jabber server and
 * communicates with it on your behalf.
 *
 * Copyright 2008 Cian Synnott.
 * 
 * $Id: ohaigent.c 28 2009-12-04 03:01:17Z cian $
 */
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include <event.h>

#include "client.h"
#include "log.h"
#include "password.h"
#include "time_format.h"
#include "utility.h"
#include "version.h"
#include "write_self.h"
#include "xmpp.h"

/* Keep all the options & usage boilerplate together here. */
static struct option longopts[] = {
  { "server", required_argument, 0, 's' },
  { "status", required_argument, 0, 'S' },
  { "port", required_argument, 0, 'p' },
  { "debug", no_argument, 0, 'D' },
  { "help", no_argument, 0, 'h' },
  { "version", no_argument, 0, 'V' },
  { 0, 0, 0, 0 }
};

static char *shortopts = "s:S:p:DhV";

static void usage(void) {
  puts(
    "Usage: ohaigent [OPTIONS] user@host/resource\n"
    "Agent for 'ohai', the jabber/xmpp commandline suite.\n"
    " -s, --server=HOST  Server to use if different to JID server part.\n"
    " -S, --status=TEXT  Initial status text (default 'ohai!').\n"
    " -p, --port=PORT    Port to use if different from jabber default.\n");
  puts(
    " -d, --debug        Be verbose in logging and remain in the foreground.\n"
    " -h, --help         Print this text.\n"
    " -V, --version      Print the version.\n"
    "See the project page at http://code.google.com/p/ohai/");
}

/* Core session objects: global-ish static linkage so we can cleanup atexit. */
static struct agent_globals {
  xmpp_session *xmpp;
  client_session *client;
  FILE *log;
  char *log_path;
  char *socket_path;
} globals = {0};

/* We may get partly through setup before exiting. Not that it matters
 * terribly, but we try to clean up properly. */
static void globals_teardown(void) {
  if (globals.client)
    client_teardown(globals.client);
  if (globals.xmpp)
    xmpp_teardown(globals.xmpp);

  /* Technically we should check fclose() == EOF, but we can hardly log
   * it if we do ... */
  if (globals.log)
    fclose(globals.log);

  if (globals.log_path)
    free(globals.log_path);
  if (globals.socket_path)
    free(globals.socket_path);
}

/* Setup ~/.ohai directory, including a new logfile. */
static void setup_working_directory(void);

/* Be a good signalling citizen. */
static void setup_signals(void);

/* Callback to capture libevent's log messages. */
static void redirect_event_logs(int severity, const char *msg);

/* Convert a string to a port number, with checking */
static int string_to_port(char *string);

int main(int argc, char *argv[]) {
  char *jid, *password;
  char *server = NULL;
  char *status = NULL;
  int port = 0;
  int debug = 0;
  pid_t child;
  int opt;

  while ((opt = getopt_long(argc, argv, shortopts, longopts, NULL)) != -1) {
    switch(opt) {
      case 's':
        server = optarg;
        break;
      case 'S':
        status = optarg;
        break;
      case 'p':
        port = string_to_port(optarg);
        break;
      case 'D':
        log_set_debug();
        log_register(stderr);
        debug = 1;
        break;
      case 'h':
        usage();
        exit(EXIT_SUCCESS);
        break;
      case 'V':
        puts("ohaigent " OHAI_VERSION);
        exit(EXIT_SUCCESS);
    }
  }

  if (argc - optind != 1) {
    usage();
    exit(EXIT_FAILURE);
  }

  jid = argv[optind];
  setup_working_directory();
  password = get_password("Password for %s: ", jid);

  if (!debug) {
    /* We don't fully daemonize here because we want HUP to kill us. */
    child = fork();
    if (-1 == child)
      log_errx(EXIT_FAILURE, "Failed to fork(): %s", strerror(errno));
    if (child != 0) {
      printf("Giving you back your terminal.\n"
             "Will exit when your controlling terminal hangs up.\n"
             "Alternatively, just 'killall ohaigent'.\n");
      exit(EXIT_SUCCESS);
    } else {
      int null;

      if (chdir("/") == -1)
        log_errx(EXIT_FAILURE, "Failed to chdir(\"/\"): %s", strerror(errno));

      if ((null = open("/dev/null", O_RDWR)) == -1)
        log_errx(EXIT_FAILURE, "Failed to open /dev/null read/write: %s",
          strerror(errno));

      if (dup2(null, STDIN_FILENO) == -1)
        log_errx(EXIT_FAILURE, "Failed to redirect stdin to /dev/null: %s",
          strerror(errno));
      if (dup2(null, STDOUT_FILENO) == -1)
        log_errx(EXIT_FAILURE, "Failed to redirect stdout to /dev/null: %s",
          strerror(errno));
      if (dup2(null, STDERR_FILENO) == -1)
        log_errx(EXIT_FAILURE, "Failed to redirect stderr to /dev/null: %s",
          strerror(errno));
    }
  }

  event_init();
  event_set_log_callback(redirect_event_logs);

  globals.xmpp = xmpp_setup(jid, password, server, port, status);
  globals.client = client_setup(globals.xmpp, globals.socket_path);
  if(atexit(globals_teardown) != 0)
    log_errx(EXIT_FAILURE, "Failed to register globals_teardown with atexit()");
  setup_signals();

  event_dispatch();

  return 0;
}

/* Signal handling functions. */

static void signal_handle_termination(int signal) {
  log_info("Signal %d caught, exiting", signal);
  if (event_loopexit(NULL) == -1) 
    log_errx(EXIT_FAILURE, "Failed to register exit with event loop");
}

static void setup_signals(void) {
  struct sigaction action;
  action.sa_handler = signal_handle_termination;
  sigemptyset (&action.sa_mask);
  action.sa_flags = 0;

  if (sigaction(SIGINT, &action, NULL) == -1)
    log_errx(EXIT_FAILURE, "Failed to install handler for SIGINT: %s",
      strerror(errno));

  if (sigaction(SIGTERM, &action, NULL) == -1)
    log_errx(EXIT_FAILURE, "Failed to install handler for SIGTERM: %s",
      strerror(errno));

  if (sigaction(SIGHUP, &action, NULL) == -1)
    log_errx(EXIT_FAILURE, "Failed to install handler for SIGHUP: %s",
      strerror(errno));
}

/* Utility functions, see prototypes for commentary. */

static void redirect_event_logs(int severity, const char *msg) {
  if (severity == _EVENT_LOG_DEBUG) log_debug("%s", msg);
  else if (severity == _EVENT_LOG_MSG) log_info("%s", msg);
  else if (severity == _EVENT_LOG_WARN) log_warn("%s", msg);
  else log_err("%s", msg);
}

static int string_to_port(char *string) {
  int result;
  char *endptr = NULL;

  result = (int) strtol(string, &endptr, 0);

  if (*string == '\0' || *endptr)
    log_errx(EXIT_FAILURE, "Failed to convert port '%s' to an integer", string);

  if (result < 1 || result > 65535)
    log_errx(EXIT_FAILURE, "Port '%s' is not between 1 and 65535", string);

  return result;
}

/* TODO(cian): Long and tedious. Break this up a little. */
static void setup_working_directory(void) {
  struct stat stat_buf;  
  char *home = getenv("HOME");
  char *working_directory;
  char *timestamp;
  FILE *logfile;

  if (NULL == home)
    log_errx(EXIT_FAILURE, "Failed to find HOME environment variable. Weird!");

  working_directory = create_path("%s/.ohai", home);
  if (NULL == working_directory)
    log_errx(EXIT_FAILURE, "Error constructing path of ~/.ohai");
  
  /* Ensure this directory exists and that its stat() info is in stat_buf. */
  if (stat(working_directory, &stat_buf) == -1) {
    if (ENOENT == errno) {
      log_info("Creating new working directory %s", working_directory);
      if(mkdir(working_directory, 0700) == -1)
        log_errx(EXIT_FAILURE, "Error creating a new working directory %s: %s",
          working_directory, strerror(errno));
      if (stat(working_directory, &stat_buf) == -1)
        log_errx(EXIT_FAILURE, "Error stat'ing %s: %s", working_directory,
          strerror(errno));
    } else {
      log_errx(EXIT_FAILURE, "Error stat'ing %s: %s", working_directory,
        strerror(errno));
    }
  }

  /* Check to be certain we're dealing with a directory. */
  if (!S_ISDIR(stat_buf.st_mode))
    log_errx(EXIT_FAILURE, "%s is not a directory", working_directory);

  /* Ensure restrictive permissions. */
  if ((S_IRWXG & stat_buf.st_mode) != 0 || (S_IRWXO & stat_buf.st_mode) != 0) {
    log_warn("%s has unrestrictive permissions 0%o, fixing.", working_directory,
      stat_buf.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
    if (chmod(working_directory, 0700) == -1)
      log_errx(EXIT_FAILURE, "Failed to chmod %s to owner-use only: %s",
        working_directory, strerror(errno));
  }

  /* Create a new logfile. We could race here, but we don't really care. */
  timestamp = time_formatted_by("%F-%T", 0);
  globals.log_path = create_path("%s/logfile.%s.%d", working_directory,
    timestamp, getpid());
  if (NULL == globals.log_path)
    log_errx(EXIT_FAILURE, "Error creating logfile path");
  free(timestamp);
  
  log_info("Logging to %s", globals.log_path);

  if ((logfile = fopen(globals.log_path, "a")) == NULL)
    log_errx(EXIT_FAILURE, "Error opening logfile %s: %s", globals.log_path,
      strerror(errno));
  if (setvbuf(logfile, NULL, _IONBF, 0) != 0)
    log_errx(EXIT_FAILURE, "Error setting logfile %s unbuffered: %s",
      globals.log_path, strerror(errno));
  if (log_register(logfile) == -1)
    log_errx(EXIT_FAILURE, "Error registering logfile %s as a log",
      globals.log_path);
  globals.log = logfile;


  /* Construct and record the socket path. */
  globals.socket_path = create_path("%s/socket", working_directory);
  if (NULL == globals.socket_path)
    log_errx(EXIT_FAILURE, "Error creating socket path");
}
